/*
 * Licensed to the Tai Ping Jin Ke
 *
 * Copyright (c) 2022 .
 * All rights reserved.
 * 项目名称：太平金科-基础组件封装层-会话管理-缺省实现
 * 版权说明：本软件属太平金融科技服务(上海)有限公司所有，在未获得太平金融科技服务(上海)有限公司正式授权情况下，任何企业和个人，不能获取、阅读、安装、传播本软件涉及的任何受知识产权保护的内容。
 */
package com.taipingframework.boot.session;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.taipingframework.boot.cache.core.CommonCacheHandler;
import com.taipingframework.boot.cache.redis.StandardCacheKeyBuilder;
import com.taipingframework.boot.session.support.ClientDetails;
import com.taipingframework.boot.web.response.status.ExceptionStatusEnum;
import com.taipingframework.utility.constant.ApplicationConstant;
import com.taipingframework.utility.exception.SystemInterrupterException;
import com.taipingframework.utility.exception.UtilityException;
import com.taipingframework.utility.thread.threadlocal.TransmittableThreadLocalHelper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Objects;

/**
 * 声明 原始会话管理处理器 的默认行为
 */
@Slf4j
abstract class OriginalSessionHandler {

    /**
     * 标准缓存(读/写)处理器
     */
    CommonCacheHandler cacheHandler;
    /**
     * JSON字符串(读/写)对象
     */
    ObjectMapper objectMapper;
    /**
     * JSON字符串(读/写)对象
     */
    ObjectMapper objectMapperForRedis;
    /**
     * HTTP请求头中唯一标识登录用户的参数名
     */
    String authTokenKey;

    OriginalSessionHandler(CommonCacheHandler cacheHandler,
                           ObjectMapper objectMapper) throws UtilityException {
        try {
            if (Objects.isNull(cacheHandler)) {
                throw new SystemInterrupterException(ExceptionStatusEnum.UNABLE_GET_CACHE_HANDLER);
            }
            this.cacheHandler = cacheHandler;

            if (Objects.isNull(objectMapper)) {
                throw new SystemInterrupterException(ExceptionStatusEnum.UNABLE_GET_OBJECT_MAPPER);
            }
            this.objectMapper = objectMapper.copy();
            this.objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

            this.objectMapperForRedis = objectMapper.copy();
            this.objectMapperForRedis.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);

            this.authTokenKey = StandardCacheKeyBuilder.getAuthTokenKey();
        } catch (Exception e) {
            throw new UtilityException(e);
        }
    }

    /**
     * 创建Session [登录]
     * <p>
     * 请求头中携带的token有效则返回当前会话对象HttpSession，
     * 无效则重新创建会话对象HttpSession并返回；
     */
    HttpSession createHttpSession(HttpServletRequest request) throws UtilityException {
        try {
            // 获取HTTP请求的远程客户端设备标识
            String clientIpAddress = TransmittableThreadLocalHelper.getEnvironment().get(ApplicationConstant.REMOTE_CLIENT_IP_KEY);
            // 尝试获取请求头中的token
            String accessToken = request.getHeader(authTokenKey);

            // 请求头中携带token非空且格式错误
            if (StringUtils.isNotEmpty(accessToken) && !ClientDetails.validSessionId(accessToken)) {
                log.error("终端显示层【{}】会话对象标识参数的格式错误：{}", clientIpAddress, accessToken);
                throw new SystemInterrupterException(ExceptionStatusEnum.INVALID_TOKEN_IN_HTTP_HEADER);
            }

            // 尝试获取当前会话对象 - 请求头中携带的token：① Access-Token为空 ② Access-Token是正确格式的UUID
            HttpSession session = request.getSession(false);
            // 当前会话对象非空则表示有效，直接返回
            if (Objects.nonNull(session)) {
                // 格式正确的Access-Token没有过期，直接返回
                log.debug("终端显示层【{}】的会话对象(session-id：{})在服务器已存在，不执行创建会话对象逻辑", clientIpAddress, accessToken);
                return session;
            }

            // 请求头中的token无效，开始创建一个新的会话
            log.debug("终端显示层【{}】未登录，准备创建会话对象", clientIpAddress);

            // 创建会话对象 - 请求头中携带的token：① Access-Token为空 ② Access-Token格式正确但已经过期
            session = request.getSession(true);

            // 将当前会话数据持久化到服务器
            String requestId = TransmittableThreadLocalHelper.getEnvironment().get(ApplicationConstant.RESPONSE_IDENTITY);
            session.setAttribute(ApplicationConstant.RESPONSE_IDENTITY, requestId);
            session.setAttribute(ApplicationConstant.REMOTE_CLIENT_IP_KEY, clientIpAddress);

            // 在ThreadLocal中存放Access-Token【在StandardCacheKeyBuilder.java中获取】
            // 我们约定：如果当前会话对象存在就能够保证在TransmittableThreadLocalHelper.getEnvironment()中保存的Access-Token一定存在！
            TransmittableThreadLocalHelper.getEnvironment().putIfAbsent(authTokenKey, session.getId());

            // 在ThreadLocal中存放当前HTTP请求的会话对象最大持续时长【在CommonCacheHandler.java中获取】
            TransmittableThreadLocalHelper.getEnvironment().putIfAbsent(ApplicationConstant.SESSION_DURATION_KEY, String.valueOf(session.getMaxInactiveInterval()));

            // 根据上下文能够推断出是基于何种场景新建的会话对象
            if (StringUtils.isNotEmpty(accessToken)) {
                log.debug("终端显示层【{}】会话对象(session-id：{})已经过期，重新创建会话对象(session-id：{})成功", clientIpAddress, accessToken, session.getId());
            } else {
                log.debug("终端显示层【{}】创建会话对象(session-id：{})成功", clientIpAddress, session.getId());
            }

            return session;
        } catch (Exception e) {
            throw new UtilityException(e);
        }
    }

    /**
     * 移除Session [注销]
     */
    boolean removeHttpSession(HttpServletRequest request) throws UtilityException {
        try {
            // 获取HTTP请求的远程客户端设备标识
            String clientIpAddress = TransmittableThreadLocalHelper.getEnvironment().get(ApplicationConstant.REMOTE_CLIENT_IP_KEY);
            // 尝试获取请求头中的token
            String accessToken = request.getHeader(authTokenKey);

            // 判断请求头中是否携带 token
            if (StringUtils.isEmpty(accessToken)) {
                log.error("终端显示层【{}】的HTTP请求头没有携带会话对象标识参数。", clientIpAddress);
                // 请求头中没有携带token
                throw new SystemInterrupterException(ExceptionStatusEnum.WITHOUT_TOKEN_IN_HTTP_HEADER);
            } else if (!ClientDetails.validSessionId(accessToken)) {
                log.error("终端显示层【{}】会话对象标识参数的格式错误：{}", clientIpAddress, accessToken);
                // 请求头中携带token，但格式错误
                throw new SystemInterrupterException(ExceptionStatusEnum.INVALID_TOKEN_IN_HTTP_HEADER);
            }

            // 尝试获取当前会话对象
            HttpSession session = request.getSession(false);
            // 当前会话对象非空则表示有效，则使其失效
            if (Objects.nonNull(session)) {
                session.invalidate();

                log.debug("终端显示层【{}】会话对象(session-id：{})注销成功", clientIpAddress, accessToken);
                return true;
            }

            // 请求头中携带token，但已经过期，nothing to do.
            log.debug("终端显示层【{}】会话对象(session-id：{})已经过期，不执行注销逻辑", clientIpAddress, accessToken);
            return false;
        } catch (Exception e) {
            throw new UtilityException(e);
        }
    }

    /**
     * 获取当前会话对象[登录标识]
     * <p>
     * 在当前HTTP请求的生命周期之内，如果HTTP请求头携带的token为空或无效，
     * 那么，无论调用createHttpSession()方法成功多少次，在调用getHttpSession()方法时，
     * 都将始终无法获取到createHttpSession()方法创建的HttpSession对象！
     * <p>
     * 因此，首次调用createHttpSession()方法成功之后，需要使用该方法返回的HttpSession对象对业务数据进行读写。
     */
    HttpSession getHttpSession(HttpServletRequest request) throws UtilityException {
        try {
            // 尝试获取当前会话对象
            HttpSession session = request.getSession(false);
            // 当前会话对象非空则表示有效，直接返回
            if (Objects.nonNull(session)) {
                return session;
            }

            /*
             * 假如，在上面步骤中没有获取到session：
             * （1）当前HTTP请求头没有携带token，需要抛异常；
             * （2）当前HTTP请求头携带的token不是UUID类型，需要抛异常；
             * （3）当前HTTP请求头携带了token，但是长时间未操作导致终端显示层的会话对象在服务器已经过期，也需要抛异常；
             */

            // 获取HTTP请求的远程客户端设备标识
            String clientIpAddress = TransmittableThreadLocalHelper.getEnvironment().get(ApplicationConstant.REMOTE_CLIENT_IP_KEY);
            // 尝试获取请求头中的token
            String accessToken = request.getHeader(authTokenKey);

            // 判断请求头中是否携带 token
            if (StringUtils.isEmpty(accessToken)) {
                log.error("终端显示层【{}】的HTTP请求头没有携带会话对象标识参数。", clientIpAddress);
                // 请求头中没有携带token
                throw new SystemInterrupterException(ExceptionStatusEnum.WITHOUT_TOKEN_IN_HTTP_HEADER);
            } else if (ClientDetails.validSessionId(accessToken)) {
                log.error("终端显示层【{}】会话对象(session-id：{})已经过期", clientIpAddress, accessToken);
                // 请求头中携带token，但已经过期
                throw new SystemInterrupterException(ExceptionStatusEnum.EXPIRED_TOKEN_IN_HTTP_HEADER);
            } else {
                log.error("终端显示层【{}】会话对象标识参数的格式错误：{}", clientIpAddress, accessToken);
                // 请求头中携带token，但格式错误
                throw new SystemInterrupterException(ExceptionStatusEnum.INVALID_TOKEN_IN_HTTP_HEADER);
            }
        } catch (Exception e) {
            throw new UtilityException(e);
        }
    }

}
